package com.minio.common.minio.service;


import com.minio.common.minio.configure.MinioConfig;
import com.minio.common.minio.utils.SnowFlake;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;


/**
 * @Author wangli
 * @Date 2023/5/23 19:01
 * @Description: minio方法
 */
@Component
@Slf4j
public class MinioService {

    @Autowired
    private MinioConfig config;
    @Resource
    private MinioClient minioClient;


    /**
     * 查看存储bucket是否存在
     *
     * @param bucketName 桶名
     * @return boolean
     */
    public Boolean bucketExists(String bucketName) {
        try {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}查询bucket是否存在失败:{}", this.getClass().getSimpleName(), e.getMessage());
            return false;
        }
    }

    /**
     * 创建存储bucket
     *
     * @param bucketName 桶名
     * @return Boolean
     */
    public Boolean makeBucket(String bucketName) {
        try {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}创建bucket失败:{}", this.getClass().getSimpleName(), e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 删除存储bucket
     *
     * @param bucketName 桶名
     * @return Boolean
     */
    public Boolean removeBucket(String bucketName) {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}删除bucket失败:{}", this.getClass().getSimpleName(), e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 获取全部bucket
     *
     * @return 桶名集合
     */
    public List<String> getAllBuckets() {
        try {
            List<Bucket> buckets = minioClient.listBuckets();
            if (buckets != null && buckets.size() > 0) {
                List<String> names = new ArrayList<>();
                buckets.forEach(b -> names.add(b.name()));
                return names;
            } else {
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}获取全部bucket失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 文件上传,配置文件指定桶
     *
     * @param file 文件
     * @return 文件名称
     */
    public String upload(MultipartFile file) {
        Assert.notNull(file, "文件不可为空");
        String bucketName = config.getBucketName();
        if (StringUtils.isBlank(bucketName)) {
            throw new IllegalArgumentException("参数错误,桶名未配置");
        }
        return upload(file, bucketName);
    }

    /**
     * 文件上传，指定桶名
     *
     * @param file       文件
     * @param bucketName 桶名
     * @return 文件名称
     */
    public String upload(MultipartFile file, String bucketName) {
        Assert.notNull(file, "文件不可为空");
        String originalFilename = file.getOriginalFilename();
        if (StringUtils.isAnyBlank(originalFilename, bucketName)) {
            throw new IllegalArgumentException("参数错误");
        }
        String fileName = originalFilename.substring(originalFilename.lastIndexOf("."));
        LocalDate localDate = LocalDate.now();
        String objectName = localDate.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/" + SnowFlake.generateId() + fileName;
        return upload(file, bucketName, objectName);
    }

    /**
     * 文件上传，指定桶名，指定存储文件名
     *
     * @param file       文件
     * @param bucketName 桶名
     * @param objectName 远程文件名称
     * @return 文件名称
     */
    public String upload(MultipartFile file, String bucketName, String objectName) {
        Assert.notNull(file, "文件不可为空");
        Assert.notNull(bucketName, "桶名不可为空");
        Assert.notNull(objectName, "minio文件名不可为空");
        try {
            Boolean exists = bucketExists(bucketName);
            if (!exists) {
                makeBucket(bucketName);
            }
            Map<String, String> map = Collections.singletonMap("original_file", file.getOriginalFilename());
            PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(objectName)
                    .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType())
                    .userMetadata(map).build();
            //文件名称相同会覆盖
            minioClient.putObject(objectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}上传失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
        return objectName;
    }

    /**
     * 上传本地文件，指定本地文件及存储文件名
     *
     * @param localFile  本地文件
     * @param objectName 远程文件名称
     * @return
     */
    public String uploadLocalFile(String localFile, String objectName) {
        String bucketName = config.getBucketName();
        if (StringUtils.isAnyBlank(localFile, bucketName)) {
            throw new IllegalArgumentException("参数错误");
        }
        return uploadLocalFile(bucketName, localFile, objectName);
    }

    /**
     * 上传本地文件，指定桶名、本地文件及存储文件名
     *
     * @param bucketName bucket名称
     * @param localFile  本地文件
     * @param objectName 远程文件名称
     * @return 文件名称
     */
    public String uploadLocalFile(String bucketName, String localFile, String objectName) {
        Assert.notNull(bucketName, "桶名不可为空");
        Assert.notNull(localFile, "本地文件不可为空");
        File file = new File(localFile);
        if (!file.exists() || !file.isFile()) {
            throw new RuntimeException("文件不存在");
        }
        log.info("{}操作文件上传...", this.getClass().getSimpleName());
        HashMap<String, String> map = new HashMap<>(1);
        if (StringUtils.isNotBlank(objectName)) {
            objectName = objectName + localFile.substring(localFile.lastIndexOf("."));
            map.put("original_file", objectName);
        } else {
            map.put("original_file", file.getName());
        }
        try {
            if (StringUtils.isBlank(objectName)) {
                String fileName = localFile.substring(localFile.lastIndexOf("."));
                LocalDate localDate = LocalDate.now();
                objectName = localDate.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/" + SnowFlake.generateId() + fileName;
            }
            Boolean exists = bucketExists(bucketName);
            if (!exists) {
                makeBucket(bucketName);
            }
            UploadObjectArgs build = UploadObjectArgs.builder().bucket(bucketName).filename(localFile).object(objectName)
                    .userMetadata(map).build();
            minioClient.uploadObject(build);
            log.info("{}操作文件上传成功", this.getClass().getSimpleName());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}上传失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
        return objectName;
    }

    /**
     * 根据指定文件和桶名预览
     *
     * @param fileName   文件名
     * @param bucketName 桶名
     * @return url
     */
    public String preview(String fileName, String bucketName) {
        // 查看文件地址
        GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(fileName).method(Method.GET).build();
        try {
            String url = minioClient.getPresignedObjectUrl(build);
            return url;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}预览失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 文件下载
     *
     * @param fileName 文件名称
     * @param res      response
     * @return Boolean
     */
    public void download(String fileName, HttpServletResponse res) {
        String bucketName = config.getBucketName();
        if (StringUtils.isAnyBlank(fileName, bucketName)) {
            throw new IllegalArgumentException("参数错误");
        }
        download(fileName, bucketName, res, false);
    }

    /**
     * 文件内嵌显示
     *
     * @param fileName 文件名称
     * @param res      response
     * @return Boolean
     */
    public void open(String fileName, HttpServletResponse res) {
        String bucketName = config.getBucketName();
        if (StringUtils.isAnyBlank(fileName, bucketName)) {
            throw new IllegalArgumentException("参数错误");
        }
        download(fileName, bucketName, res, true);
    }

    /**
     * 文件下载，指定桶名
     *
     * @param fileName 文件名称
     * @param fileName 桶名称
     * @param open     true 内嵌显示 false 下载
     * @param res      response
     */
    public void download(String fileName, String bucketName, HttpServletResponse res, boolean open) {
        if (StringUtils.isAnyBlank(fileName, bucketName)) {
            throw new IllegalArgumentException("参数错误");
        }
        log.info("{}操作文件下载开始", this.getClass().getSimpleName());
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName)
                .object(fileName).build();
        StatObjectArgs statObjectArgs = StatObjectArgs.builder().bucket(bucketName)
                .object(fileName).build();
        try (GetObjectResponse response = minioClient.getObject(objectArgs)) {
            // 获取自定义元数据
            String myMetadataValue = minioClient.statObject(statObjectArgs).userMetadata().get("original_file");
            if (StringUtils.isBlank(myMetadataValue)) {
                myMetadataValue=fileName;
            }
            byte[] buf = new byte[1024];
            int len;
            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
                while ((len = response.read(buf)) != -1) {
                    os.write(buf, 0, len);
                }
                os.flush();
                byte[] bytes = os.toByteArray();
                res.setCharacterEncoding("utf-8");

                if (open) {
                    String fileType = myMetadataValue.substring(myMetadataValue.lastIndexOf(".") + 1);
                    //设置文件ContentType类型
                    if ("jpg,jepg,gif,png".contains(fileType)) {  //图片类型
                        res.setContentType("image/" + fileType);
                    } else if ("pdf".contains(fileType)) {         //pdf类型
                        res.setContentType("application/pdf");
                    } else if ("mp2,mp3".contains(fileType)) {         //pdf类型
                        res.setContentType("audio/" + fileType);
                    } else if ("mp2v,mp4".contains(fileType)) {         //pdf类型
                        res.setContentType("video/" + fileType);
                    } else if ("mpeg".contains(fileType)) {         //pdf类型
                        res.setContentType("video/mpg");
                    } else {                                      //自动判断下载文件类型
                        res.setContentType("multipart/form-data");
                    }
                    res.setHeader("Content-Disposition", "inline;fileName=" + URLEncoder.encode(myMetadataValue, "UTF-8"));
                } else {
                    res.addHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(myMetadataValue, "UTF-8"));
                }

                try (ServletOutputStream stream = res.getOutputStream()) {
                    stream.write(bytes);
                    stream.flush();
                } catch (ClientAbortException e) {
                    log.info("客户端主动关闭连接");
                }
                log.info("{}操作文件下载成功", this.getClass().getSimpleName());
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}文件下载失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
    }

    public void downloadSlice(String fileName, HttpServletResponse res,
                              HttpServletRequest request) throws Exception {
        String bucketName = config.getBucketName();
        if (StringUtils.isAnyBlank(fileName, bucketName)) {
            throw new IllegalArgumentException("参数错误");
        }
        downloadSlice(fileName, bucketName, res, request);
    }

    public void downloadSlice(String filename, String bucketName, HttpServletResponse res, HttpServletRequest request) {
        if (StringUtils.isAnyBlank(filename, bucketName)) {
            throw new IllegalArgumentException("参数错误");
        }
        log.info("{}操作文件分片下载开始", this.getClass().getSimpleName());
        String range = request.getHeader("Range");
        //获取文件信息
        StatObjectArgs build = StatObjectArgs.builder().bucket(bucketName).object(filename).build();
        StatObjectResponse statObjectResponse = null;

        try {
            statObjectResponse = minioClient.statObject(build);
            // 获取自定义元数据
            String myMetadataValue = statObjectResponse.userMetadata().get("original_file");
            if (StringUtils.isNotBlank(myMetadataValue)) {
                filename = myMetadataValue;
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}文件分片下载失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
        //开始下载位置
        long startByte = 0;
        //结束下载位置
        long endByte = statObjectResponse.size() - 1;
        //有range的话
        if (StringUtils.isNotBlank(range) && range.contains("bytes=") && range.contains("-")) {
            range = range.substring(range.lastIndexOf("=") + 1).trim();
            String[] ranges = range.split("-");
            try {
                //判断range的类型
                if (ranges.length == 1) {
                    //类型一：bytes=-2343
                    if (range.startsWith("-")) {
                        endByte = Long.parseLong(ranges[0]);
                    }
                    //类型二：bytes=2343-
                    else if (range.endsWith("-")) {
                        startByte = Long.parseLong(ranges[0]);
                    }
                }
                //类型三：bytes=22-2343
                else if (ranges.length == 2) {
                    startByte = Long.parseLong(ranges[0]);
                    endByte = Long.parseLong(ranges[1]);
                }
            } catch (NumberFormatException e) {
                startByte = 0;
                endByte = statObjectResponse.size() - 1;
            }
            log.info("开始下载位置{},结束下载位置{}", startByte, endByte);
        }

        //要下载的长度
        long contentLength = endByte - startByte + 1;
        //文件类型
        String contentType = request.getServletContext().getMimeType(filename);
        //各种响应头设置
        //支持断点续传，获取部分字节内容：
        res.setCharacterEncoding("utf-8");
        res.setHeader("Accept-Ranges", "bytes");
        //http状态码要为206：表示获取部分内容
        res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        res.setContentType(contentType);
        res.setHeader("Last-Modified", statObjectResponse.lastModified().toString());
        //inline表示浏览器直接使用，attachment表示下载，fileName表示下载的文件名
        try {
            res.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(filename, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            log.error("{}不支持的编码异常:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
        res.setHeader("Content-Length", String.valueOf(contentLength));
        //Content-Range，格式为：[要下载的开始位置]-[结束位置]/[文件总大小]
        res.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + statObjectResponse.size());
        res.setHeader("ETag", "\"".concat(statObjectResponse.etag()).concat("\""));

        try {
            GetObjectResponse clientObject = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(statObjectResponse.bucket())
                            .object(statObjectResponse.object())
                            .offset(startByte)
                            .length(contentLength)
                            .build());
            ServletOutputStream stream = res.getOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            FastByteArrayOutputStream os = new FastByteArrayOutputStream();
            while ((len = clientObject.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            try {
                stream.write(os.toByteArray());
                stream.flush();
            } catch (ClientAbortException e) {
                log.info("客户端主动关闭连接");
            }
            log.info("{}操作文件分片下载成功", this.getClass().getSimpleName());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}文件分片下载失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }

    }

    /**
     * 文件下载到本地
     *
     * @param fileName 文件名称
     * @param localPath 本地保存文件的地址（存在的路径）
     */
    public void downloadToLocal(String fileName, String localPath) {
        String bucketName = config.getBucketName();
        if (StringUtils.isAnyBlank(bucketName, fileName, localPath)) {
            throw new IllegalArgumentException("参数错误");
        }
        File file = new File(localPath);
        if (!file.exists()||!file.isDirectory()) {
            throw new RuntimeException("路径不存在");
        }
        this.download2Local(fileName, bucketName, localPath,null);
    }

    /**
     * 文件下载到本地,指定路径(路径不存在则创建)
     * @param fileName 文件名称
     * @param bucketName 桶名
     * @param path 路径
     * @param reName 重命名（要包括后缀名）
     */
    public void downloadToLocal(String fileName, String bucketName, String path,String reName) {
        if (StringUtils.isAnyBlank(fileName, path)) {
            throw new IllegalArgumentException("参数错误");
        }
        if (StringUtils.isBlank(bucketName)) {
             bucketName = config.getBucketName();
            Assert.notNull(bucketName, "桶名不可为空");
        }
        File file = new File(path);
        if (!file.exists()) {
            file.mkdirs();
        }
        //文件重命名了
        if (StringUtils.isNotBlank(reName)) {
            if (reName.lastIndexOf(".")<0) {
                throw new IllegalArgumentException("重命名需要包括文件类型");
            }
        }
        this.download2Local(fileName,bucketName,path,reName);
    }
    /**
     * 文件下载到本地
     *
     * @param fileName 文件名称
     * @param bucketName 桶名称
     * @param localPath 本地保存文件的地址（存在的路径）
     * @param reName 重命名
     */
    private void download2Local(String fileName, String bucketName, String localPath,String reName) {
        if (StringUtils.isAnyBlank(fileName, bucketName, localPath)) {
            throw new IllegalArgumentException("参数错误");
        }
        File file = new File(localPath);
        if (!file.exists() || !file.isDirectory()) {
            throw new IllegalArgumentException("路径错误");
        }
        try {
            if (StringUtils.isBlank(reName)) {
                StatObjectArgs statObjectArgs = StatObjectArgs.builder().bucket(bucketName).object(fileName).build();
                // 获取自定义元数据
                String myMetadataValue = minioClient.statObject(statObjectArgs).userMetadata().get("original_file");
                if (StringUtils.isBlank(myMetadataValue)) {
                    myMetadataValue = fileName.substring(fileName.lastIndexOf(File.separator) + 1);
                }
                localPath = localPath + File.separator + myMetadataValue;
            } else {
                localPath=localPath+ File.separator + reName;
            }
            File newFile = new File(localPath);
            if (newFile.exists()) {
                newFile.delete();
            }
            // 调用downloadObject方法下载对象
            log.info("{}操作文件开始下载", this.getClass().getSimpleName());
            DownloadObjectArgs builder = DownloadObjectArgs.builder().bucket(bucketName).object(fileName).filename(localPath).build();
            minioClient.downloadObject(builder);
            log.info("{}操作文件下载成功", this.getClass().getSimpleName());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}文件下载失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 下载文件流
     *
     * @param fileName 文件名称
     */
    public InputStream downloadStream(String fileName) {
        //获取文件流
        String bucketName = config.getBucketName();
        GetObjectArgs build = GetObjectArgs.builder().bucket(bucketName).object(fileName).build();
        try {
            return minioClient.getObject(build);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}文件下载流失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 批量下载同一个桶内文件并zip压缩到指定地址
     * @param bucketName
     * @param fileNames
     * @param downloadPath
     * @throws MinioException
     * @throws IOException
     * @return 返回压缩包全路径
     */
    private String downloadAndCompressFiles(String bucketName, List<String> fileNames, String downloadPath) {
        if (CollectionUtils.isEmpty(fileNames)) {
            throw new IllegalArgumentException("参数错误,文件名称不可为空");
        }
        if (StringUtils.isBlank(bucketName)) {
            bucketName = config.getBucketName();
            if (StringUtils.isBlank(bucketName)) {
            throw new IllegalArgumentException("参数错误,桶名未配置");
            }
            if (!bucketExists(bucketName)) {
                throw new IllegalArgumentException("参数错误,桶不存在");
            }
        }
        // 如果 downloadPath 为空，则设置默认值  System.getProperty("user.dir")+File.separator+"download
        String parentPath = (StringUtils.isNotBlank(downloadPath)) ? downloadPath.trim() : System.getProperty("user.dir")+File.separator+"download";
        downloadPath= parentPath+File.separator+SnowFlake.generateId();
        File directory = new File(downloadPath);
        if (!directory.exists()) {directory.mkdirs();}
        try {
            //下载到本地
            for (String fileName : fileNames) {
                downloadToLocal(fileName,downloadPath);
            }
            // 创建压缩文件
            String compressFileName = parentPath +File.separator+SnowFlake.generateId()+ "files.zip";
            //目标
            FileOutputStream fos = new FileOutputStream(compressFileName);
            ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(fos));
            File[] files = directory.listFiles();
            // 将下载的文件添加到压缩文件中
            for (File file : files) {
                ZipEntry zipEntry = new ZipEntry(file.getName());
                zos.putNextEntry(zipEntry);
                FileInputStream fis = new FileInputStream(file);
                byte[] buffer = new byte[1024];
                int length;
                while ((length = fis.read(buffer)) > 0) {
                    zos.write(buffer, 0, length);
                }
                fis.close();
                zos.closeEntry();
            }
            // 关闭流连接
            zos.close();
            fos.close();
            return compressFileName;
        } catch (IOException e) {
            e.printStackTrace();
            log.error("{}文件批量下载流失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }finally {
            // 删除临时下载的文件
            try {
                FileUtils.deleteDirectory(directory);
            } catch (IOException e) {
                e.printStackTrace();
                log.error("{}文件删除失败:{}", this.getClass().getSimpleName(), e.getMessage());
                throw new RuntimeException(e);
            }
        }
    }

    public String downloadAndCompressFiles(List<String> fileNames, String downloadPath) {
        return this.downloadAndCompressFiles(config.getBucketName(),fileNames,downloadPath);
    }

    public String downloadAndCompressFiles(List<String> fileNames) {
        return this.downloadAndCompressFiles(config.getBucketName(),fileNames,null);
    }

    /**
     * 查看桶里文件对象
     *
     * @param bucketName
     * @return 存储bucket内文件对象信息
     */
    public List<Item> listObjects(String bucketName) {
        try {
            Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
            List<Item> items = new ArrayList<>();
            for (Result<Item> result : results) {
                items.add(result.get());
            }
            return items;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}查看桶里文件对象失败:{}", this.getClass().getSimpleName(), e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 删除
     *
     * @param fileName
     * @param bucketName
     * @return
     */
    public Boolean remove(String fileName, String bucketName) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileName).build());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("{}删除文件对象失败:{}", this.getClass().getSimpleName(), e.getMessage());
            return false;
        }
        return true;
    }

}
